package jp.co.nes.awssupport.business.utility;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.regions.Regions;
import com.amazonaws.services.ec2.model.ReservedInstances;
import com.amazonaws.services.elasticache.model.ReservedCacheNode;
import com.amazonaws.services.elasticsearch.model.ReservedElasticsearchInstance;
import com.amazonaws.services.elasticsearch.model.ReservedElasticsearchInstancePaymentOption;
import com.amazonaws.services.rds.model.ReservedDBInstance;
import com.amazonaws.services.redshift.model.ReservedNode;

import jp.co.nes.awssupport.business.bean.ReservedInputBean;
import jp.co.nes.awssupport.business.bean.ReservedResultBean;
import jp.co.nes.awssupport.business.constant.AwsProductName;
import jp.co.nes.awssupport.business.constant.Duration;
import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.bean.AwsKeyBean;
import jp.co.nes.awssupport.common.utility.AppInfoUtil;
import jp.co.nes.awssupport.common.utility.AwsEc2Helper;
import jp.co.nes.awssupport.common.utility.AwsElastiCacheHelper;
import jp.co.nes.awssupport.common.utility.AwsElasticsearchHelper;
import jp.co.nes.awssupport.common.utility.AwsRdsHelper;
import jp.co.nes.awssupport.common.utility.AwsRedshiftHelper;
import jp.co.nes.awssupport.common.utility.DateUtil;

/**
 * リザーブドヘルパーです。
 */
public class ReservedHelper {

    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(ReservedHelper.class);

    /**
     * デフォルトコンストラクタです。
     */
    public ReservedHelper() {
    }

    /**
     * ReservedResultを返却します。
     *
     * @param reservedInputBean ReservedInput
     * @return ReservedResult
     */
    public ReservedResultBean getReservedResultBean(ReservedInputBean reservedInputBean) {
        String id = reservedInputBean.getId();
        if (StringUtils.isEmpty(id)) {
            throw new CommonException("id is null or empty(id=" + id + ")");
        }

        String productName = reservedInputBean.getProductName();
        if (StringUtils.isEmpty(productName)) {
            // productNameがnull or emptyでないものが、この処理を呼び出しているはずなので、
            // productNameがnull or emptyだった場合は例外をスローする。
            throw new CommonException("productName is null or empty(productName=" + productName + ")");
        }

        AwsKeyBean akBean = reservedInputBean.getAwsKeyBean();
        if (!AwsProductNameUtil.isEquals(AwsProductName.DYNAMO_DB, productName)) {
            checkAwsKeyBean(reservedInputBean.getAwsKeyBean());
        }

        Regions regions = reservedInputBean.getRegions();
        ReservedResultBean resultBean = null;
        if (AwsProductNameUtil.isEquals(AwsProductName.EC2, productName)) {
            ReservedInstances ri = getReservedInstances(regions, akBean, id);
            if (ri == null) {
                // idに該当するReservedInstancesが存在しなかった場合
                throw new CommonException("ReservedInstances not exist(id=" + id + ")");
            }
            resultBean = new ReservedResultBean(AwsProductName.EC2);
            resultBean.setId(ri.getReservedInstancesId());
            resultBean.setRegionCode(RegionUtil.getRegionCode(regions.getName()));
            resultBean.setMultiAZ(null);
            resultBean.setProductDescription(ri.getProductDescription());
            resultBean.setType(ri.getInstanceType());
            resultBean.setStart(DateUtil.toZonedDateTime(ri.getStart()));
            resultBean.setEnd(DateUtil.toZonedDateTime(ri.getEnd()));
            resultBean.setOfferingType(ri.getOfferingType());
            resultBean.setOfferingClass(ri.getOfferingClass());
            resultBean.setDuration(ri.getDuration().intValue());
            resultBean.setCount(toBigDecimal(ri.getInstanceCount()));
            resultBean.setFixedPrice(toBigDecimal(ri.getFixedPrice()));
        } else if (AwsProductNameUtil.isEquals(AwsProductName.ELASTI_CACHE, productName)) {
            ReservedCacheNode rc = getReservedCacheNode(regions, akBean, id);
            if (rc == null) {
                // idに該当するReservedCacheNodeが存在しなかった場合
                throw new CommonException("ReservedCacheNode not exist(id=" + id + ")");
            }
            resultBean = new ReservedResultBean(AwsProductName.ELASTI_CACHE);
            resultBean.setId(rc.getReservedCacheNodeId());
            resultBean.setRegionCode(RegionUtil.getRegionCode(regions.getName()));
            resultBean.setMultiAZ(null);
            resultBean.setProductDescription(rc.getProductDescription());
            resultBean.setType(rc.getCacheNodeType());
            ZonedDateTime start = DateUtil.toZonedDateTime(rc.getStartTime());
            resultBean.setStart(start);
            resultBean.setEnd(getEnd(start, rc.getDuration()));
            resultBean.setOfferingType(rc.getOfferingType());
            resultBean.setOfferingClass(null);
            resultBean.setDuration(rc.getDuration().intValue());
            resultBean.setCount(toBigDecimal(rc.getCacheNodeCount()));
            resultBean.setFixedPrice(toBigDecimal(rc.getFixedPrice()));
        } else if (AwsProductNameUtil.isEquals(AwsProductName.REDSHIFT, productName)) {
            ReservedNode rn = getReservedNode(regions, akBean, id);
            if (rn == null) {
                // idに該当するReservedNodeが存在しなかった場合
                throw new CommonException("ReservedNode not exist(id=" + id + ")");
            }
            resultBean = new ReservedResultBean(AwsProductName.REDSHIFT);
            resultBean.setId(rn.getReservedNodeId());
            resultBean.setRegionCode(RegionUtil.getRegionCode(regions.getName()));
            resultBean.setMultiAZ(null);
            resultBean.setProductDescription(null);
            resultBean.setType(rn.getNodeType());
            ZonedDateTime start = DateUtil.toZonedDateTime(rn.getStartTime());
            resultBean.setStart(start);
            resultBean.setEnd(getEnd(start, rn.getDuration()));
            resultBean.setOfferingType(rn.getOfferingType());
            resultBean.setOfferingClass(null);
            resultBean.setDuration(rn.getDuration().intValue());
            resultBean.setCount(toBigDecimal(rn.getNodeCount()));
            resultBean.setFixedPrice(toBigDecimal(rn.getFixedPrice()));
        } else if (AwsProductNameUtil.isEquals(AwsProductName.RDS, productName)) {
            ReservedDBInstance rd = getReservedDBInstance(regions, akBean, id);
            if (rd == null) {
                // idに該当するReservedDBInstanceが存在しなかった場合
                throw new CommonException("ReservedDBInstance not exist(id=" + id + ")");
            }
            resultBean = new ReservedResultBean(AwsProductName.RDS);
            resultBean.setId(rd.getReservedDBInstanceId());
            resultBean.setRegionCode(RegionUtil.getRegionCode(regions.getName()));
            Boolean rdMultiAZ = rd.getMultiAZ();
            if (rdMultiAZ != null) {
                String multiAZ = "0";
                if (rdMultiAZ.booleanValue()) {
                    multiAZ = "1";
                }
                resultBean.setMultiAZ(multiAZ);
            }
            resultBean.setProductDescription(rd.getProductDescription());
            resultBean.setType(rd.getDBInstanceClass());
            ZonedDateTime start = DateUtil.toZonedDateTime(rd.getStartTime());
            resultBean.setStart(start);
            resultBean.setEnd(getEnd(start, rd.getDuration()));
            resultBean.setOfferingType(rd.getOfferingType());
            resultBean.setOfferingClass(null);
            resultBean.setDuration(rd.getDuration().intValue());
            resultBean.setCount(toBigDecimal(rd.getDBInstanceCount()));
            resultBean.setFixedPrice(toBigDecimal(rd.getFixedPrice()));
        } else if (AwsProductNameUtil.isEquals(AwsProductName.DYNAMO_DB, productName)) {
            // DynamoDBの場合、fixedPriceの設定は不要。
            String regionCode = null;
            String multiAZ = null;
            String productDescription = null;
            String type = null;
            String offeringType = null;
            String offeringClass = null;
            resultBean = new ReservedResultBean(AwsProductName.DYNAMO_DB);
            resultBean.setId(id);
            resultBean.setRegionCode(regionCode);
            resultBean.setMultiAZ(multiAZ);
            resultBean.setProductDescription(productDescription);
            resultBean.setType(type);
            resultBean.setStart(reservedInputBean.getUsageStartDate());
            resultBean.setEnd(reservedInputBean.getUsageEndDate());
            resultBean.setOfferingType(offeringType);
            resultBean.setOfferingClass(offeringClass);
            Long duration = getDurationDynamoDB(reservedInputBean);
            resultBean.setDuration(duration.intValue());
            BigDecimal usageQuantity = reservedInputBean.getUsageQuantity();
            resultBean.setCount(usageQuantity);
        } else if (AwsProductNameUtil.isEquals(AwsProductName.ELASTIC_SEARCH_SERVICE, productName)) {
            ReservedElasticsearchInstance rs = getReservedElasticsearchInstance(regions, akBean, id);
            if (rs == null) {
                // idに該当するReservedElasticsearchInstanceが存在しなかった場合
                throw new CommonException("ReservedElasticsearchInstance not exist(id=" + id + ")");
            }
            resultBean = new ReservedResultBean(AwsProductName.ELASTIC_SEARCH_SERVICE);
            resultBean.setId(rs.getReservedElasticsearchInstanceId());
            resultBean.setRegionCode(RegionUtil.getRegionCode(regions.getName()));
            resultBean.setMultiAZ(null);
            resultBean.setProductDescription(null);
            resultBean.setType(rs.getElasticsearchInstanceType());
            ZonedDateTime start = DateUtil.toZonedDateTime(rs.getStartTime());
            resultBean.setStart(start);
            resultBean.setEnd(getEnd(start, rs.getDuration()));

            String offeringType = null;
            ReservedElasticsearchInstancePaymentOption paymentOption = ReservedElasticsearchInstancePaymentOption
                    .fromValue(rs.getPaymentOption());
            if (paymentOption == ReservedElasticsearchInstancePaymentOption.ALL_UPFRONT) {
                offeringType = "All Upfront";
            } else if (paymentOption == ReservedElasticsearchInstancePaymentOption.PARTIAL_UPFRONT) {
                offeringType = "Partial Upfront";
            } else if (paymentOption == ReservedElasticsearchInstancePaymentOption.NO_UPFRONT) {
                offeringType = "No Upfront";
            } else {
                // ReservedElasticsearchInstancePaymentOptionクラスに修正が入って、列挙子が追加されない限り、
                // 通常、本処理は実行されない。
                // 追加された場合は、ワーニングを出力し、toString()で返却される文字列を設定する。
                // ワーニングの内容を見て、リザーブド管理に出力された内容、ソース修正等の対応を実施する。
                logger.warn("paymentOption is wrong(paymentOption=" + paymentOption.toString() + ")");
                offeringType = paymentOption.toString();
            }
            resultBean.setOfferingType(offeringType);

            resultBean.setOfferingClass(null);
            resultBean.setDuration(rs.getDuration().intValue());
            resultBean.setCount(toBigDecimal(rs.getElasticsearchInstanceCount()));
            resultBean.setFixedPrice(toBigDecimal(rs.getFixedPrice()));
        } else {
            // 製品名がEC2, RDS, Redshift、DynamoDB、Elasticsearch以外は例外をスローする。
            throw new CommonException("productName is wrong(productName=" + productName + ")");
        }
        return resultBean;
    }

    /**
     * ReservedInstancesを返却します。
     *
     * @param regions リージョン
     * @param akBean AwsKey
     * @param id リザーブドインスタンスのID
     * @return ReservedInstances
     */
    protected ReservedInstances getReservedInstances(Regions regions, AwsKeyBean akBean, String id) {
        String accessKey = akBean.getAccessKey();
        String secretKey = akBean.getSecurityKey();
        AwsEc2Helper helper = new AwsEc2Helper(regions, accessKey, secretKey);
        return helper.getReservedInstances(id);
    }

    /**
     * ReservedCacheNodeを返却します。
     *
     * @param regions リージョン
     * @param akBean AwsKey
     * @param id リザーブドインスタンスのID
     * @return ReservedCacheNode
     */
    protected ReservedCacheNode getReservedCacheNode(Regions regions, AwsKeyBean akBean, String id) {
        String accessKey = akBean.getAccessKey();
        String secretKey = akBean.getSecurityKey();
        AwsElastiCacheHelper helper = new AwsElastiCacheHelper(regions, accessKey, secretKey);
        return helper.getReservedCacheNode(id);
    }

    /**
     * ReservedNodeを返却します。
     *
     * @param regions リージョン
     * @param akBean AwsKey
     * @param id リザーブドインスタンスのID
     * @return ReservedNode
     */
    protected ReservedNode getReservedNode(Regions regions, AwsKeyBean akBean, String id) {
        String accessKey = akBean.getAccessKey();
        String secretKey = akBean.getSecurityKey();
        AwsRedshiftHelper helper = new AwsRedshiftHelper(regions, accessKey, secretKey);
        return helper.getReservedNode(id);
    }

    /**
     * ReservedDBInstanceを返却します。
     *
     * @param regions リージョン
     * @param akBean AwsKey
     * @param id リザーブドインスタンスのID
     * @return ReservedDBInstance
     */
    protected ReservedDBInstance getReservedDBInstance(Regions regions, AwsKeyBean akBean, String id) {
        String accessKey = akBean.getAccessKey();
        String secretKey = akBean.getSecurityKey();
        AwsRdsHelper helper = new AwsRdsHelper(regions, accessKey, secretKey);
        return helper.getReservedDBInstance(id);
    }

    /**
     * ReservedElasticsearchInstanceを返却します。
     *
     * @param regions リージョン
     * @param akBean AwsKey
     * @param id リザーブドインスタンスのID
     * @return ReservedElasticsearchInstance
     */
    protected ReservedElasticsearchInstance getReservedElasticsearchInstance(Regions regions, AwsKeyBean akBean,
            String id) {
        String accessKey = akBean.getAccessKey();
        String secretKey = akBean.getSecurityKey();
        AwsElasticsearchHelper helper = new AwsElasticsearchHelper(regions, accessKey, secretKey);
        return helper.getReservedElasticsearchInstance(id);
    }

    /**
     * AwsKeyの情報をチェックします。
     *
     * @param akBean AwsKey
     */
    private void checkAwsKeyBean(AwsKeyBean akBean) {
        if (akBean == null) {
            throw new CommonException("accessKey and securityKey is not exist");
        }
        if (StringUtils.isEmpty(akBean.getAccessKey())) {
            throw new CommonException("accessKey is null or empty");
        }
        if (StringUtils.isEmpty(akBean.getSecurityKey())) {
            throw new CommonException("securityKey is null or empty");
        }
    }

    /**
     * BigDecimal型に変換します。
     *
     * @param value 値
     * @return BigDecimal型に変換した値
     */
    private BigDecimal toBigDecimal(Integer value) {
        BigDecimal result = new BigDecimal("0.0000000000");
        if (value != null && value != 0) {
            result = new BigDecimal(value);
        }
        return result;
    }

    /**
     * BigDecimal型に変換します。
     *
     * @param value 値
     * @return BigDecimal型に変換した値
     */
    private BigDecimal toBigDecimal(Float value) {
        BigDecimal result = new BigDecimal("0.0000000000");
        if (value != null && value != 0) {
            result = new BigDecimal(value.toString());
            result = result.setScale(10, RoundingMode.HALF_UP);
        }
        return result;
    }

    /**
     * BigDecimal型に変換します。
     *
     * @param value 値
     * @return BigDecimal型に変換した値
     */
    private BigDecimal toBigDecimal(Double value) {
        BigDecimal result = new BigDecimal("0.0000000000");
        if (value != null && value != 0) {
            result = new BigDecimal(value.toString());
            result = result.setScale(10, RoundingMode.HALF_UP);
        }
        return result;
    }

    /**
     * 終了日時を返却します。
     *
     * @param start 開始日時
     * @param duration duration(秒)
     * @return 終了日時
     */
    private ZonedDateTime getEnd(ZonedDateTime start, Integer duration) {
        ZonedDateTime end = null;
        if (start == null) {
            // 開始日時がnullの場合は例外をスローする。
            // 通常は発生しない想定。
            throw new CommonException("start is null");
        }

        int second = 0;
        if (duration == null) {
            // duration(秒)がnullの場合は例外をスローする。
            // 通常は発生しない想定。
            throw new CommonException("duration is null");
        } else {
            second = duration;
        }
        end = start.plusSeconds(second);
        return end;
    }

    /**
     * 期間を返却します(DynamoDB)。
     *
     * @param ReservedInputBean ReservedInputBean
     * @return 期間
     */
    private Long getDurationDynamoDB(ReservedInputBean reservedInputBean) {
        long differenceTime = AppInfoUtil.getLongValAndMinChk("UsageType", "DifferenceTime", 10L, 0L);
        if (differenceTime >= 31536000) {
            throw new CommonException("differenceTime >= 31536000");
        }
        long oneYearSecondMin = Duration.ONE_YEAR_SECOND.longSecond() - differenceTime;
        long oneYearSecondMax = Duration.ONE_YEAR_SECOND.longSecond() + differenceTime;
        long threeYearSecondMin = Duration.THREE_YEAR_SECOND.longSecond() - differenceTime;
        long threeYearSecondMax = Duration.THREE_YEAR_SECOND.longSecond() + differenceTime;
        long untilValue = reservedInputBean.getUsageStartDate().until(reservedInputBean.getUsageEndDate(),
                ChronoUnit.SECONDS);
        Long duration = null;
        if (oneYearSecondMin <= untilValue && oneYearSecondMax >= untilValue) {
            duration = new Long(Duration.ONE_YEAR_SECOND.longSecond());
        }
        if (threeYearSecondMin <= untilValue && threeYearSecondMax >= untilValue) {
            duration = new Long(Duration.THREE_YEAR_SECOND.longSecond());
        }
        if (duration == null) {
            throw new CommonException("duration is wrong");
        }
        return duration;
    }
}
